import {
  BleError,
  BleManager as BleEXManager,
  Characteristic,
  Device,
  DeviceId,
  LogLevel,
  ScanMode,
  State,
  Subscription,
} from 'react-native-ble-plx'
import BleCharacteristicsConstants from '@CONSTANTS/BleCharacteristicsConstants'
import BleConstants, { BleConnectivityError, BleConnectivityStatus, ResponseState } from '@CONSTANTS/BleConstants'
import BluUtils from '@UTILS/BluUtils'
import { base64 } from '@pier88health/p8h-common'
import DeviceResponseParser from '@UTILS/DeviceResponseParser'
import { DeviceResponse } from '@TYPES/BleTypes'
import DeviceConfigurationInfo from '@UTILS/DeviceConfigurationInfo'
import DeviceStatusParser from './DeviceStatusParser'

type PeripheralConnectionStateListener = (status: BleConnectivityStatus, device?: Device|null) => void

class BleManager {
  private peripheralConnectionStateListener: Array<PeripheralConnectionStateListener> = []

  private bleManager?: BleEXManager

  private timer: number = 1

  private connectDevice?: Device

  private isSearching ?: boolean

  private searchDevices: Array<Device> = []

  private subscriptions: Subscription[] = []

  init = () => {
    if (!this.bleManager) {
      this.bleManager = new BleEXManager()
    }
  }

  addPeripheralConnectionStateListener = (peripheralConnectionStateListener: PeripheralConnectionStateListener) => {
    this.peripheralConnectionStateListener.push(peripheralConnectionStateListener)
  }

  removePeripheralConnectionStateListener = (peripheralConnectionStateListener: PeripheralConnectionStateListener) => {
    this.peripheralConnectionStateListener.push(peripheralConnectionStateListener)
    const index = this.peripheralConnectionStateListener.indexOf(peripheralConnectionStateListener)
    if (index >= 0) {
      this.peripheralConnectionStateListener.splice(index, 1)
    }
  }

  removeAllPeripheralConnectionStateListener = () => {
    this.peripheralConnectionStateListener = []
  }

  private emitConnectionState = (status: BleConnectivityStatus, device?: Device) => {
    console.log('emitConnectionState', status)
    if (this.peripheralConnectionStateListener?.length === 0) return
    this.peripheralConnectionStateListener.forEach((item) => {
      item?.(status, status === BleConnectivityStatus.Searched ? device : null)
    })
  }

  getBleManager = () => {
    if (!this.bleManager) {
      this.bleManager = new BleEXManager()
      this.bleManager.setLogLevel(LogLevel.Verbose)
    }
    return this.bleManager
  }

  timeout = async (seconds: number = 3) => {
    return new Promise<void>((resolve) => {
      setTimeout(() => {
        resolve()
      }, seconds * 1000)
    })
  }

  // 监听蓝牙开关
  onStateChange = (listener: (newState: State) => void, emitCurrentState?: boolean):
    Subscription => {
    console.log('onStateChange')
    const subscription = this.getBleManager().onStateChange((state: State) => {
      console.log('state', state)
      listener?.(state)
    }, emitCurrentState)
    this.subscriptions.push(subscription)
    return subscription
  }

  readCharacteristic = (characteristicUuid: string) => {
    if (!this.connectDevice) return null
    return this.connectDevice?.readCharacteristicForService(BleCharacteristicsConstants.SERVICE_UUID, characteristicUuid)
  }

  writeCharacteristic = (characteristicUuid: string, base64Value: string) => {
    if (!this.connectDevice) return null
    return this.getBleManager()
      .writeCharacteristicWithResponseForDevice(
        this.connectDevice.id,
        BleCharacteristicsConstants.SERVICE_UUID,
        characteristicUuid,
        base64Value,
      )
  }

  readDeviceResponse = async (from: null | Characteristic): Promise<DeviceResponse> => {
    if (!this.connectDevice) return {} as DeviceResponse
    console.log('readDeviceResponse: from', from)
    const status = await this.readCharacteristic(BleCharacteristicsConstants.STATUS_CHARACTERISTIC_UUID)
    console.log('BleManager: readDeviceResponse', status)
    const response = await this.readCharacteristic(BleCharacteristicsConstants.RESPONSE_CHARACTERISTIC_UUID)
    console.log('BleManager: readDeviceResponse', response)
    const data = DeviceResponseParser.parse(base64.decode(response?.value))
    return data
  }

  sendTime = async (): Promise<DeviceResponse | null> => {
    try {
      const response = await this.writeCharacteristic(BleCharacteristicsConstants.COMMAND_CHARACTERISTIC_UUID, BluUtils.getTimeData())
      console.log('sendUuid', response)
      return this.readDeviceResponse(response)
    } catch (e) {
      throw Error(BleConnectivityError.AuthenticationFailed)
    }
  }

  sendUuid = async (): Promise<DeviceResponse> => {
    try {
      const uuid = await BluUtils.getUuidData()
      const response = await this.writeCharacteristic(BleCharacteristicsConstants.COMMAND_CHARACTERISTIC_UUID, uuid)
      console.log('BleManager: sendUuid:', response)
      return this.readDeviceResponse(response)
    } catch (e) {
      throw Error(BleConnectivityError.AuthenticationFailed)
    }
  }

  sendPassword = async () => {
    try {
      const password = await BluUtils.getPasswordData()
      const response = await this.writeCharacteristic(BleCharacteristicsConstants.COMMAND_CHARACTERISTIC_UUID, password)
      console.log('BleManager: sendPassword', response)
      return this.readDeviceResponse(response)
    } catch (e) {
      throw Error(BleConnectivityError.AuthenticationFailed)
    }
  }

  writeCommand = async (value: string) => {
    const response = await this.writeCharacteristic(BleCharacteristicsConstants.COMMAND_CHARACTERISTIC_UUID, base64.encode(value))
    console.log('BleManager: writeCommand', value, response)
    return this.readDeviceResponse(response)
  }

  readCommand = async (uuid: string) => {
    const response = await this.readCharacteristic(uuid)
    console.log('BleManager: readCommand', uuid, response)
    return base64.decode(response?.value)
  }

  // 治疗过程中读取状态
  readDeviceStatus = async () => {
    const status = await this.readCharacteristic(BleCharacteristicsConstants.STATUS_CHARACTERISTIC_UUID)
    return DeviceStatusParser.parse(base64.decode(status?.value))
  }

  private connectToDevice = async (
    deviceIdentifier: DeviceId,
    listener: (error: BleError | null, device: Device | null) => void,
  ) => {
    this.emitConnectionState(BleConnectivityStatus.Connecting)
    try {
      const device = await this.getBleManager()
        .connectToDevice(deviceIdentifier, { requestMTU: 512, autoConnect: false, timeout: BleConstants.SCAN_TIME_OUT * 1000 })
        .catch(() => {
          throw Error(BleConnectivityError.ConnectionTimedOut)
        })

      this.connectDevice = device
      const result = await this.connectDevice?.discoverAllServicesAndCharacteristics()
      console.log('BleManager: connectToDevice: discoverAllServicesAndCharacteristics', result)
      await this.timeout()
      device.onDisconnected((error, deviceItem) => {
        console.log('onDisconnected', error, deviceItem)
        this.emitConnectionState(BleConnectivityStatus.Disconnected)
      })
      const status = await this.readCharacteristic(BleCharacteristicsConstants.STATUS_CHARACTERISTIC_UUID)
      const responseUuId = await this.sendUuid()
      console.log('BleManager: connectToDevice', status, responseUuId)
      if (responseUuId.responseStatus === ResponseState.NACK) {
        const responsePass = await this.sendPassword()
        console.log('BleManager: connectToDevice: Got NACK, sending password', responsePass)
      } else if (responseUuId.responseStatus === ResponseState.ACK) {
        // 连接成功继续下一步
      } else {
        throw new Error(BleConnectivityError.IllegalStateException)
      }
      const responseTime = await this.sendTime()
      if (!responseTime || Object.keys(responseTime).length === 0) {
        throw new Error(BleConnectivityError.IllegalStateException)
      } else if (responseTime?.responseStatus === ResponseState.ACK) {
        this.emitConnectionState(BleConnectivityStatus.ConnectedAndAuthenticated)
      } else if (responseTime?.responseStatus === ResponseState.NACK) {
        throw Error(BleConnectivityError.AuthenticationFailed)
      }
      // Reading device info
      const deviceInfoBleResponse = await this.readCharacteristic(BleCharacteristicsConstants.INFO_CHARACTERISTIC_UUID)
      const serialNumberResponse = await this.readCharacteristic(BleCharacteristicsConstants.SERIAL_COMMAND_CHARACTERISTIC_UUID)
      const info = new DeviceConfigurationInfo(deviceInfoBleResponse, serialNumberResponse)
      const deviceInfo = info.getDeviceInfo()
      const serialNumber = info.getSerialNumber()
      console.log('BleManager: connectToDevice: Reading device info', deviceInfo, serialNumber)
      if (deviceInfo.isError) {
        this.emitConnectionState(BleConnectivityStatus.ConnectedAndAuthenticated)
      } else {
        this.emitConnectionState(BleConnectivityStatus.Connected)
        listener?.(null, this.connectDevice)
        console.log('BleManager: connectToDevice: Device configure success', deviceInfo)
      }
    } catch (error) {
      // @ts-ignore
      const message = error?.message as BleConnectivityError
      console.log('BleManager: connectToDevice: catch', error)
      switch (message) {
        case BleConnectivityError.ConnectionTimedOut:
          this.emitConnectionState(BleConnectivityStatus.ConnectionTimedOut)
          break
        case BleConnectivityError.AuthenticationFailed:
          this.emitConnectionState(BleConnectivityStatus.AuthenticationFailed)
          break
        default: {
          this.emitConnectionState(BleConnectivityStatus.Disconnected)
        }
      }
    }
  }

  private onDeviceDisconnected = (deviceIdentifier: string, listener: (error: BleError | null, device: Device | null) => void): Subscription => {
    return this.getBleManager().onDeviceDisconnected(deviceIdentifier, (error: BleError | null, device: Device | null) => {
      console.log('BleManager: onDeviceDisconnected', error, device)
      listener?.(error, device)
    })
  }

  connect = (address: string | null, listener: (error: BleError | null, device: Device | null) => void) => {
    if (!address) {
      this.startDeviceScan((error, device) => {
        this.emitConnectionState(BleConnectivityStatus.Searched, device)
        this.connectToDevice(device?.id as DeviceId, listener)
      })
      return
    }
    this.connectToDevice(address, listener)
  }

  private startDeviceScan = (listener: (error: BleError | null, scannedDevice: Device | null) => void) => {
    console.log('BleManager: startDeviceScan')
    if (this.timer) {
      clearTimeout(this.timer)
    }
    this.emitConnectionState?.(BleConnectivityStatus.Searching)
    this.stopDeviceScan()
    this.isSearching = true
    this.timer = setTimeout(() => {
      this.isSearching = false
      this.stopDeviceScan()
      clearTimeout(this.timer)
      this.searchDevices.sort((pre, next) => {
        return (next.rssi || 0) - (pre.rssi || 0)
      })
      const device = this.searchDevices[0]
      if (device) {
        listener(null, device)
        return
      }
      this.emitConnectionState?.(BleConnectivityStatus.SearchingTimedOut)
      this.emitConnectionState?.(BleConnectivityStatus.Disconnected)
    }, BleConstants.SCAN_TIME_OUT * 1000)
    this.getBleManager().startDeviceScan(
      [BleCharacteristicsConstants.SERVICE_UUID],
      { scanMode: ScanMode.LowLatency, allowDuplicates: false },
      (error, info) => {
        console.log('BleManager:search device: ', error, info)
        if (!info || !this.isSearching) return
        this.searchDevices.push(info)
      },
    )
  }

  stopDeviceScan = () => {
    console.log('stopDeviceScan')
    this.getBleManager().stopDeviceScan()
  }

  disabledAllListener = () => {
    const curBleManager = this.getBleManager()
    if (curBleManager) {
      curBleManager.destroy()
    }
  }

  remove = (item: Subscription) => {
    if (!item) return
    item.remove()
    const index = this.subscriptions.findIndex((d) => d === item)
    if (index === -1) return
    this.subscriptions.splice(index, 1)
  }

  destroy = () => {
    this.getBleManager().destroy()
    if (Array.isArray(this.subscriptions)) {
      this.subscriptions.forEach((item) => {
        item.remove()
      })
      this.subscriptions = []
    }
  }
}

const bleManager = new BleManager()
export default bleManager
